package im.composer.audioservers.flac;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javaFlacEncoder.FLACEncoder;
import javaFlacEncoder.StreamConfiguration;

import javax.sound.sampled.AudioFormat;

import org.jaudiolibs.audioservers.AudioClient;
import org.jaudiolibs.audioservers.AudioConfiguration;
import org.jaudiolibs.audioservers.AudioServer;
import org.kc7bfi.jflac.FLACDecoder;
import org.kc7bfi.jflac.PCMProcessor;
import org.kc7bfi.jflac.metadata.StreamInfo;
import org.kc7bfi.jflac.util.ByteData;
import org.tritonus.share.sampled.FloatSampleTools;

public class FlacAudioServer implements AudioServer, PCMProcessor {

	private final String host;
	private final int port;
	private final AudioClient client;
	private Socket s;
	private InputStream in;
	private OutputStream out;
	private final FLACEncoder encoder = new FLACEncoder();
	private FLACDecoder decoder;
	private AudioConfiguration context;
	private AudioFormat format = null;
	private final List<FloatBuffer> cache = Collections.synchronizedList(new ArrayList<FloatBuffer>());
	private long nanosecond_per_frame;
	private boolean active = false;

	public FlacAudioServer(String host, int port, AudioConfiguration context, AudioClient client) {
		this.host = host;
		this.port = port;
		this.client = client;
		this.context = context;
		nanosecond_per_frame = (long) (context.getMaxBufferSize() / context.getSampleRate() * Math.pow(10, 9));
	}

	@Override
	public void run() throws Exception {
		initialize();
		runImpl();
	}

	private void initialize() throws IOException {
		s = new Socket(host, port);
		in = s.getInputStream();
		out = s.getOutputStream();
		decoder = new FLACDecoder(in);
		decoder.addPCMProcessor(this);
		int bufz = context.getMaxBufferSize();
		encoder.setOutputStream(new NonSeekableFLACOutputStream(out));
		encoder.setStreamConfiguration(new StreamConfiguration(context.getOutputChannelCount(), bufz, bufz, (int) context.getSampleRate(), 24));
		encoder.openFLACStream();
	}

	@Override
	public AudioConfiguration getAudioContext() {
		return context;
	}

	@Override
	public void processStreamInfo(StreamInfo streamInfo) {
		format = streamInfo.getAudioFormat();
	}

	private void runImpl() throws InterruptedException {
		long last = System.nanoTime();
		while (active) {
			List<FloatBuffer> outputs = prepareOutput();
			active = client.process(last, prepareInput(), outputs, context.getMaxBufferSize());
			writeOutput(outputs);
			TimeUnit.NANOSECONDS.sleep(nanosecond_per_frame);
		}
	}

	private List<FloatBuffer> prepareInput() {
		List<FloatBuffer> list = new ArrayList<>(context.getInputChannelCount());
		synchronized (cache) {
			if (cache.isEmpty()) {
				for (int i = 0; i < context.getInputChannelCount(); i++) {
					list.add(FloatBuffer.wrap(new float[context.getMaxBufferSize()]));
				}
			} else {
				list.addAll(list);
			}
		}
		return Collections.unmodifiableList(list);
	}

	private List<FloatBuffer> prepareOutput() {
		List<FloatBuffer> list = new ArrayList<>(context.getOutputChannelCount());
		for (int i = 0; i < context.getOutputChannelCount(); i++) {
			list.add(FloatBuffer.wrap(new float[context.getMaxBufferSize()]));
		}
		return Collections.unmodifiableList(list);
	}

	private void writeOutput(List<FloatBuffer> outputs) {
		float ratio = (float) Math.pow(2, 24);
		int[] arr = new int[context.getInputChannelCount() * context.getMaxBufferSize()];
		int k = 0;
		for (int i = 0; i < context.getMaxBufferSize(); i++) {
			for (int j = 0; j < context.getInputChannelCount(); j++) {
				float sample = outputs.get(j).get(i);
				int _sample = (int) (sample * ratio);
				arr[k] = _sample;
				k++;
			}
		}
		encoder.addSamples(arr, k);
		try {
			encoder.encodeSamples(encoder.fullBlockSamplesAvailableToEncode(), false);
			out.flush();
		} catch (IOException e) {
		}
	}

	@Override
	public void processPCM(ByteData pcm) {
		if (format == null) {
			return;
		}
		byte[] bin = pcm.getData();
		int chan = format.getChannels();
		List<float[]> list = new ArrayList<float[]>(chan);
		for (int i = 0; i < chan; i++) {
			list.add(new float[context.getMaxBufferSize()]);
		}
		FloatSampleTools.byte2float(bin, 0, list, 0, context.getMaxBufferSize(), format);
		synchronized (cache) {
			cache.clear();
			for (float[] arr : list) {
				cache.add(FloatBuffer.wrap(arr));
			}
		}
	}

	@Override
	public boolean isActive() {
		return active;
	}

	@Override
	public void shutdown() {
		active = false;
		try {
			in.close();
		} catch (IOException e) {
		}
		try {
			out.flush();
		} catch (IOException e) {
		}
		try {
			out.close();
		} catch (IOException e) {
		}
		try {
			s.close();
		} catch (IOException e) {
		}
	}

}
